// Copyright 2013 Stephane SOPPERA
//
// This file is part of cgi-to-fastcgi.
//
// cgi-to-fastcgi is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// cgi-to-fastcgi is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with cgi-to-fastcgi.  If not, see <http://www.gnu.org/licenses/>.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http/cgi"
	"net/http/fcgi"
	"os"
	"os/exec"
	"path/filepath"
)

// network type command line option
var netwk = flag.String("n", "tcp", "network type: tcp, tcp4, tcp6, unix or unixpacket")

// local network address command line option
var laddr = flag.String("a", "127.0.0.1:8078", "local network address; for tcp network type using \":1234\" will listen on both IPv4 and IPv6 port 1234 for all network interfaces")

// root URI prefix command line option
var root = flag.String("r", "", "root URI prefix, empty for \"/\"; the SCRIPT_NAME given to the CGI")

var wdir = flag.String("d", "", "working directory of CGI application; by default directory of the application")

// path of the optional configuration command line option
var confPath = flag.String("c", "", "path of JSON configuration file (see below)")

// Print usage informations for command line parsing
func usage() {
	fmt.Fprintf(os.Stderr, "Usage: %s [options] [--] cgi-program [cgi-program args]\n\n", os.Args[0])
	fmt.Fprintf(os.Stderr, "Options:\n")
	flag.PrintDefaults()
	fmt.Fprintf(os.Stderr, `
JSON Configuration File:

  The optional JSON configuration file can be used as a replacement of
  command line options; when both set it overrides the command line options.
  
  Moreover JSON file may contains options, such as environment
  variables, that are not available on command line.
  
  The syntax is illustrated in following example:
	{
		"network": "tcp",
		"address": ":1234",
		"root": "/test/of/root",
		"cgi": [ "my-cgi-program", "arg1", "arg2"],
		"env": {
			"A": "XXX"
		},
        "dir": "/the/working/directory"
	}
  
  This example contains all possibile keys but none of them are
  mandatory.
  The "env" property is a dictionary of additional environment
  variables to pass to the CGI application (in this case the variable
  "A" will have value "XXX").

  The "cgi" property corresponds to arguments of the command line
  (first is the CGI application, the remainder is its arguments).

  The "dir" property points to the working directory to use when
  running the CGI.
`)
}

// change default flag.Usage function to use local usage() function
func init() {
	flag.Usage = usage
}

func main() {
	// parse command line
	flag.Parse()

	// build configuration from command line first (except Cgi member initialized later)
	conf := &struct {
		Network string
		Address string
		Root    string
		Cgi     []string
		Env     map[string]string
		Dir     string
	}{}
	conf.Network = *netwk
	conf.Address = *laddr
	conf.Root = *root
	conf.Dir = *wdir

	// override with optional configuration file
	if *confPath != "" {
		// read JSON data
		confData, err := ioutil.ReadFile(*confPath)
		if err != nil {
			log.Fatal("Fail to read "+*confPath+" configuration:", err)
			return // logically unreachable
		}

		// parse JSON data
		err = json.Unmarshal(confData, conf)
		if err != nil {
			log.Fatal("Fail to parse "+*confPath+" JSON configuration:", err)
			return // logically unreachable
		}
	}

	// test that the program name has been provided when no
	// configuration file is used
	if conf.Cgi == nil {
		conf.Cgi = flag.Args()
	}

	// validate arguments (either command line or configuration file)
	// are not empty
	if len(conf.Cgi) < 1 {
		fmt.Fprintf(os.Stderr, "at least one argument is expected: the program path\n")
		usage()
		os.Exit(2)
		return // logically unreachable
	}

	// check if the program is a path or a single name; in case of
	// single name try to find it in the PATH and replace it
	if d, _ := filepath.Split(conf.Cgi[0]); d == "" {
		exePath, err := exec.LookPath(conf.Cgi[0])
		if err != nil {
			log.Fatal("Can't find in PATH an executable for "+conf.Cgi[0]+" name:", err)
			return // logically unreachable
		}
		log.Println("Found " + conf.Cgi[0] + " in PATH as: " + exePath)
		conf.Cgi[0] = exePath
	}

	// convert conf.Env map into an array of key=value strings (the
	// type expected by cgi.Handler)
	env := make([]string, 0, len(conf.Env))
	for k, v := range conf.Env {
		env = append(env, k+"="+v)
	}

	// try to create the listener with input parameters
	ln, err := net.Listen(conf.Network, conf.Address)

	if err != nil {
		log.Fatal("Fail to listen on network "+conf.Network+", address "+conf.Address+" :", err)
		return // logically unreachable
	}

	// log correct initialization of the server
	log.Println("FastCGI server started on network " + conf.Network + ", address " + conf.Address)

	// instantiate the CGI handler that spawns the CGI
	// script/application sub-processes
	h := &cgi.Handler{
		Path: conf.Cgi[0],
		Args: conf.Cgi[1:],
		Root: conf.Root,
		Env:  env,
		Dir:  conf.Dir,
	}

	// run the FastCGI server
	fcgi.Serve(ln, h)
}
